package maintain

import ( "github.com/garyburd/redigo/redis"
	"fmt" 
	"time"
	"log"
	"strconv"
	"strings"
	"errors"
	"os"
	"os/exec"
	"net" )

const (
	EXIT_FLAG_MAIN_LOOP				= 1
	EXIT_FLAG_HEART_BEAT_LOOP = 2
	EXIT_FLAG_SUBSCRIBE_LOOP  = 4
	EXIT_FLAG_ALL = EXIT_FLAG_MAIN_LOOP | EXIT_FLAG_HEART_BEAT_LOOP | EXIT_FLAG_SUBSCRIBE_LOOP
)

type WatchDogData struct {
	RedisData *RedisData
	SubscribeTcpConn *net.TCPConn
	SubscribeConn *redis.PubSubConn
	CmdChan *chan string
	Maps *Maps
	ExitFlags int
	timestampUpdateAt int64
	myDataInRedis *WatchDog
	reconnecting bool
	programInfo *ProgramInfo
	dataDir string
}

type ProgramArgsFunc func(wd *WatchDog) ([] string, error)

type ProgramInfo struct {
	Name string
	Args []string
	Dir string
	ArgsFunc ProgramArgsFunc
}

func (data *WatchDogData) CopyConn(other *WatchDogData) {
	data.RedisData = other.RedisData
	data.SubscribeTcpConn = other.SubscribeTcpConn
	data.SubscribeConn = other.SubscribeConn
	data.CmdChan = other.CmdChan
	data.Maps = other.Maps
	data.ExitFlags = other.ExitFlags
	data.timestampUpdateAt = other.timestampUpdateAt
	data.myDataInRedis = other.myDataInRedis
}

func (data *WatchDogData) MyID() int {
	if (*data).myDataInRedis != nil {
		return (*data).myDataInRedis.Id
	}
	return 0
}

func (data *WatchDogData) MyTTL() int {
	if (*data).myDataInRedis != nil {
		return (*data).myDataInRedis.Ttl
	}
	return 1
}

func Conn4WatchDog(serverAddress string, cmdChan *chan string, programInfo *ProgramInfo, dataDir string) (data *WatchDogData, err error) {
	var srvAddr *net.TCPAddr
	srvAddr, err = net.ResolveTCPAddr("tcp4", serverAddress)
	if err!= nil { return }
	return Conn4WatchDogByAddress(srvAddr, cmdChan, programInfo, dataDir)
}

func Conn4WatchDogByAddress(srvAddr *net.TCPAddr, cmdChan *chan string, programInfo *ProgramInfo, dataDir string) (data *WatchDogData, err error) {
	err = nil
	if cmdChan == nil || programInfo == nil || len(dataDir) == 0 { 
		err = errors.New("Conn4WatchDog() Param error, cmdChan or programInfo or dataDir is nil")
		return
	}
	err = os.MkdirAll(dataDir, os.ModeDir)
	if err != nil {
		return
	}
	data, err = conn(srvAddr)
	if err != nil { 
		return
	}

	(*data).CmdChan = cmdChan
	(*data).programInfo = programInfo
	(*data).dataDir = dataDir
	return
}

func conn(srvAddr *net.TCPAddr) (data *WatchDogData, err error) {
	err = nil
	data = new(WatchDogData)
	(*data).RedisData, err = ConnByAddress(srvAddr)
	if err != nil { 
		defer data.Close(); 
		return
	}

	err = (*data).update()
	if err != nil { 
		err = errors.New("Conn4WatchDogByAddress(): update error:" + err.Error())
		defer data.Close(); 
		return
	}

	(*data).SubscribeTcpConn, err = net.DialTCP("tcp", nil, data.RedisData.ServerAddress)
	if err != nil { 
		err = errors.New("Conn4WatchDogByAddress(): net.DialTCP() error:" + err.Error())
		defer data.Close(); 
		return
	}

	(*data).SubscribeConn = &redis.PubSubConn{redis.NewConn(data.SubscribeTcpConn, 5 * time.Second, 5 * time.Second)}
	(*(*data).SubscribeConn).Subscribe("cmd:" + strconv.Itoa(data.MyID()))
	return
}

func (data *WatchDogData) stopAllProgram() {
	if (*data).myDataInRedis != nil {
		value := *(*data).myDataInRedis
		for _, srv := range value.Servers {
			// Stop srv
			proc, err := os.FindProcess(srv.Pid)
			if err == nil {
				safeExitPrograss(proc, 15 * time.Second)
			}
		}
	}
}

func (data *WatchDogData) Close() {
	(*data).stopAllProgram()
	if (*data).RedisData != nil {
		(*data).RedisData.Close()
		(*data).RedisData = nil
	}
	if (*data).SubscribeConn != nil {
		(*data).SubscribeConn.Close()
		(*data).SubscribeConn = nil
	}
}

func (data *WatchDogData) Reconn() (err error) {
	if (*data).RedisData == nil {
		err = errors.New("Reconn() error, Can't reconnect after close")
		return
	} else {
		(*data).RedisData.Close()
	}
	if (*data).SubscribeConn != nil {
		(*data).SubscribeConn.Close()
		(*data).SubscribeConn = nil
	}

	var newData *WatchDogData
	newData, err = conn((*(*data).RedisData).ServerAddress)
	if err != nil {	fmt.Println("WatchDogData::Reconn() Conn4WatchDogByAddress() Failed! error:", err); return }
	data.CopyConn(newData)
	return
}

func (data *WatchDogData) update() (err error) {
	maps := new(Maps)
	err = (*data).RedisData.UpdateMaps(maps)
	if err != nil { 
		err = errors.New("WatchDogData::update(): RedisData.UpdateMaps() error:" + err.Error())
		return
	}

	if (*maps).WatchDogMap == nil {
		err = errors.New("WatchDogData::update(): now watchdog")
		return
	}

	var myData *WatchDog
	wdMap := (*maps).WatchDogMap
	localIp := (*(*data).RedisData).LocalIp
	for _, value := range (*wdMap) {
		if value.InIp == localIp {
			if myData == nil {
				myData = value
			} else {
				log.Println("WatchDogData::update() duplilcate watch dog IP", localIp)
			}
		}
	}
	if myData == nil {
		log.Println("Not found my ip(" + localIp  + ") in watch dog config list")
		if (*data).myDataInRedis != nil {
			err = errors.New("Detect my config been deleted from redis!!! Please don't do it!!! Stop me first!!!")
			return
		}
	}

	(*data).myDataInRedis = myData
	(*data).Maps = maps
	return 
}

func (data *WatchDogData) keepRunning() bool {
	return (data.ExitFlags & EXIT_FLAG_MAIN_LOOP) == 0
}

func (data *WatchDogData) heartBeatLoop() {
	pre := time.Now().Unix()
	for data.keepRunning() {
		now := time.Now().Unix()
		if pre + int64(data.MyTTL()) > now {
			time.Sleep(300 * time.Microsecond)
			continue
		} else {
			(*(*data).CmdChan) <- "__hb__"
			pre = now
		}
	}
	(*(*data).CmdChan) <- "__hb_exit__"
}

func (data *WatchDogData) subscribeLoop() {
	for data.keepRunning() {
		c := (*data).SubscribeConn
		if c != nil {
			switch n := c.Receive().(type) {
			case redis.Message:
				fmt.Printf("Message: %s %s\n", n.Channel, n.Data)
				(*(*data).CmdChan) <- string(n.Data)
			case redis.PMessage:
				fmt.Printf("PMessage: %s %s %s\n", n.Pattern, n.Channel, n.Data)
			case redis.Subscription:
				fmt.Printf("Subscription: %s %s %d\n", n.Kind, n.Channel, n.Count)
			case error:
				fmt.Printf("testSubscribe() error: %v\n", n)
				time.Sleep(time.Second)
			}
		} else {
			// Reconnecting
			time.Sleep(time.Second)
		}
	}
	(*(*data).CmdChan) <- "__ss_exit__"
}

func (data *WatchDogData) MainLoop() {
	data.ExitFlags = 0

	// Start heart beat loop
	go data.heartBeatLoop()
	// Start subscribe loop
	go data.subscribeLoop()

	var err error

	for data.ExitFlags != EXIT_FLAG_ALL {
		// Waiting command
		cmd := <-(*data.CmdChan)
		cmds := strings.Split(cmd, ",")
		switch strings.ToLower(cmds[0]) {
			case "exit":
				log.Println("Received exit command")
				data.ExitFlags |= EXIT_FLAG_MAIN_LOOP
			case "__hb_exit__":
				log.Println("__hb_exit__ command")
				data.ExitFlags |= EXIT_FLAG_HEART_BEAT_LOOP
			case "__ss_exit__":
				log.Println("__ss_exit__ command")
				data.ExitFlags |= EXIT_FLAG_SUBSCRIBE_LOOP
			case "__hb__":
				err = data.heartBeat()
				if err != nil {
					log.Println("Heartbeat error: ", err)
					go data.reconnLoop()
				}
			case "new_reserve_server":
				err = data.newReserveServer()
				if err != nil {
					log.Println("Create reserver server error: ", err)
				}
			case "dump":
				(*(*data).Maps).Dump()
			default:
				log.Println("Unknown command received:", cmd)
		}
	}
}

func (data *WatchDogData) heartBeat() (err error){
	now := time.Now().Unix()
	if (*data).timestampUpdateAt + 60 < now {
		// Update timestamp diff
		err = (*data).RedisData.UpdateTimestamp()
		if err != nil { return }
		(*data).timestampUpdateAt = now
	}

	// Update all
	fmt.Printf("redis: %+v\n", (*data).RedisData)
	err = data.update()
	if err != nil {
		log.Println("heartbeat update from redis failed")
		return
	}

	fmt.Println("todo: heartbeat")
	myid := (*data).MyID()
	if myid != 0 {
		// Todo: get load
//		load := 0.1
		_, err = (*(*(*data).RedisData).Conn).Do("HMSET", "wd:" + strconv.Itoa(myid), "status", WatchDogStatusReady, "load", "0.1", "hb", now + (*(*data).RedisData).TimeDiff)
		if err != nil {
			log.Println("heartbeat write to redis server failed")
			return
		}

		// Check program
		data.checkProgram()
	}
	return 
}

func (data *WatchDogData) reconnLoop(){
	if data.reconnecting { return }
	data.reconnecting = true
	var err error
	wait := time.Second
	for {	
		err = data.Reconn()
		if err != nil {
			// Reconnect failed, retry
			time.Sleep(wait)
			if wait < 32 {
				wait = wait * 2
			}
		} else {
			break
		}
	}
	data.reconnecting = false
}

func (data *WatchDogData) checkProgram() {
/*
	wd := (*data).myDataInRedis
	if wd == nil { return }

	rerverServerNumber := 0
	for _, srv := range wd.Servers {
		if srv.Status == ServerStatusReserve {
			rerverServerNumber++
		}

		if srv.Status != ServerStatusClose {
			log.Printf("To FindProcess(%d)\n", srv.Pid)
			// Check server process status
			p, err := os.FindProcess(srv.Pid)
			if err != nil || p == nil {
				log.Printf("Process[PID: %d, SID: %d] exit anomaly\n", srv.Pid, srv.Id)
				
				// Update server state in redis
				srv.Status = ServerStatusClose
				srv.Load = 1.0
				(*(*(*data).RedisData).Conn).Do("HMSET", "srv:" + strconv.Itoa(srv.Id), "status", ServerStatusClose, "load", "1.0")
			} else {
				log.Printf("p: %+v\n", p)
				if srv.Status == ServerStatusReserve {
					rerverServerNumber++
				}
			}
		} else {
			p, err := os.FindProcess(srv.Pid)
			if err == nil {
				safeExitPrograss(p, 10 * time.Second)
			}
			(*(*(*data).RedisData).Conn).Do("SREM", "slist", srv.Id)
			(*(*(*data).RedisData).Conn).Do("DEL", "srv:" + strconv.Itoa(srv.Id))
		}
	}

	fmt.Printf("wd.ReserveServerNumber: %d, rerverServerNumber: %d\n", wd.ReserveServerNumber, rerverServerNumber)
	programInfo := *(*data).programInfo
	var err error
	for wd.ReserveServerNumber > rerverServerNumber {
		// Todo, Ip information set in server programs
//		_, err = (*(*(*data).RedisData).Conn).Do("HMSET", "srv:" + strconv.Itoa(srvId), "status", ServerStatusReserve, "load", "0.0", 
//																							"wdId", (*data).MyID(), "pid", cmd.Process.Pid )
//		if err != nil {
//			log.Println("Write server info in redis error: ", err)
//			safeExitPrograss(cmd.Process, 30 * time.Second)
//			break
//		}
//		_, err = (*(*(*data).RedisData).Conn).Do("SADD", "slist", srvId)
		rerverServerNumber++
	}
*/
	return
}

func (data *WatchDogData) newReserveServer() (err error) {
	wd := (*data).myDataInRedis
	programInfo := *(*data).programInfo
	// Get new server ID
	var srvId int
	srvId, err = (*data).allocServerId()
	if err != nil {
			log.Println("Get New server ID! error: ", err)
			return
	}
	cmd := exec.Command(programInfo.Name)
	if programInfo.ArgsFunc != nil {
		var dynamicArgs []string
		dynamicArgs, err = programInfo.ArgsFunc(wd)
		if err != nil {
			log.Println("Get dynamic args failed! error: ", err)
			return
		}
		cmd.Args = append(programInfo.Args, dynamicArgs...)
	} else {
		cmd.Args = programInfo.Args
	}
	cmd.Args = append(cmd.Args, []string{"-ID", strconv.Itoa(srvId), "-WatchDogID", strconv.Itoa(wd.Id)}...)
	cmd.Dir = programInfo.Dir
	err = cmd.Run()
	if err != nil {
		log.Println("Start reserve program failed: ", err)
	}
	return
}

func (data *WatchDogData) allocServerId() (srvId int, err error) {
	srvId, err = redis.Int((*(*(*data).RedisData).Conn).Do("INCR", "srvIdAlloc"))
	if err != nil {
		log.Println("getNewServerId failed")
	}
	return
}

func safeExitPrograss(p *os.Process, wait time.Duration) {
	err := p.Signal(os.Interrupt)
	if err != nil {
		p.Kill()
	} else {
		pre := time.Now()
		exited := false
		for pre.Add(wait).After(time.Now()) {
			_, err = os.FindProcess(p.Pid)
			if err != nil {
				exited = true
				break
			}
			time.Sleep(time.Second)
		}
		if !exited {
			p.Kill()
		}
	}
	p.Release()
}
